iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

諸神也搖頭的 Legacy Code: 30天 .NET 工程師生存之道系列 第 21

Day 21 -「斯庫拉與卡律布狄斯」識破 Monster Method 的真面目(上):條列與糾纏

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260821/20182564fRN54DIk66.png

特洛伊戰爭打完之後,其他人差不多都回家鄉了,奧德修斯卻開始了一趟超級漫長的返鄉之旅,在海上繞了好多年,就是怎樣都回不了家。

後來,他準備離開女巫喀耳刻住的島時,喀耳刻先提醒他:

「前面有一條很麻煩的海峽,兩邊各有一種完全不同的危險。」

一邊住著怪物斯庫拉,她躲在岩壁上的洞穴裡,長了六條長長的脖子,每條脖子上都有一顆頭,每顆頭裡有三排牙齒,只要有船從附近經過,她就會六顆頭一起伸出來,一口一個,直接從甲板上抓走六名水手。

另一邊是卡律布狄斯,一個巨大到足以把整艘船吞掉的海怪,她每天會把海水吸進去三次,再吐出來三次,吸水的時候,海面會整個往下陷,甚至能看到底下的海床,船要是靠得太近,基本上是整艘船直接消失。

這件事情奧德修斯並沒有跟船員們講,因為奧德修斯早就下定決心要走哪一邊了....

當船經過海峽時,船員們全都盯著另一邊那個瘋狂翻騰的漩渦,深怕下一秒整艘船就被吸進去,結果就在大家忙著看卡律布狄斯的時候,斯庫拉從岩洞裡伸出六顆頭,一下子就抓走了六個人。

最後,船成功通過海峽。

Monster Method 指的是一個 method 膨脹到塞進太多步驟、規則與例外流程,導致我們很難單獨理解其中一塊,更不敢確定修改一個地方會不會傷到另一段,問題不只是行數很多,而是資料流、控制流程與不同職責開始糾纏在一起。

就像奧德修斯遇到的怪獸一樣,都不是好惹的,而我們除了要有膽識之外,也需要先辨識它們的型態後,再處理它們,今天我們來看看最常見的兩種 Monster Method。


條列式

第一種叫 Bulleted Method,它的外觀不醜,可以看到它查資料、計算、判斷、寫入,一個區塊接著一個區塊往下排,把畫面拉遠一點看,就像一份條列清單或是腳本。

今日範例 中有一座停車場的計費系統 ParkingFeeService,結帳 method 長這樣:

public decimal CalculateParkingFee(string ticketId, DateTime exitTime)
{
    var ticket = _ticketRepo.Get(ticketId);
    var lot = _lotRepo.Get(ticket.LotId);
    // 計時以 30 分鐘為單位無條件進位,刷卡機精度只到半小時
    var units = Math.Ceiling((exitTime - ticket.EntryTime).TotalMinutes / 30.0);

    decimal baseFee = lot.UnitRate * (decimal)units;

    // 週末全天加收 80 元平台費,不管停幾小時,物業合約條款
    decimal platformFee = 0m;
    if (ticket.EntryTime.DayOfWeek == DayOfWeek.Saturday
        || ticket.EntryTime.DayOfWeek == DayOfWeek.Sunday)
        platformFee = 80m;

    // 夜間加成兩成:22:00 到次日 07:00,補夜班人力成本
    decimal nightSurcharge = 0m;
    if (ticket.EntryTime.Hour >= 22 || ticket.EntryTime.Hour < 7)
        nightSurcharge = baseFee * 0.2m;

    // 月租會員前三小時免費,超出才計費,合約條款不得折回
    var member = _memberRepo.FindByPlate(ticket.LicensePlate);
    decimal memberDiscount = 0m;
    if (member != null && member.HasMonthlyPass)
    {
        var freeUnits = Math.Min(units, 6); // 6 units = 3 小時
        memberDiscount = lot.UnitRate * (decimal)freeUnits;
    }

    // 管理費只抽基本停車費,不含加成和平台費,當年跟物業談的規則
    decimal managementFee = baseFee * 0.05m;

    // ...後面還有活動日加成、開發票、寫結帳紀錄,每段又是二三十行

    return baseFee + platformFee + nightSurcharge + managementFee - memberDiscount;
}

運氣好的話,前人還會像這樣留註解或空行幫我們分段,表面上每一段都很完整,很容易讓人產生幻覺,順著整個程式碼的資料流重新看一次,就會發現:

資料 在哪裡產生 後面誰還需要它
units 進場與離場時間換算 基本費、月租折扣
baseFee 單價乘上計費單位 夜間加成、管理費、最後總額
ticket.EntryTime 停車票資料 計時、週末平台費、夜間加成
ticket.LicensePlate 停車票資料 查詢月租會員

假設明天真的把「夜間加成」的部分抽出去,新的 method 就必須接收進場時間與 baseFee,如果抽「月租折扣」,則會需要單價、units、車牌和會員資料存取,如果只是照著程式碼無腦硬切,新的 method 很快就會需要一長串參數,這不一定代表不能抽取,而是提醒我們畫面上的區塊,不一定就是好的拆分邊界。

所以遇到條列式程式碼的閱讀重點是:

每一個區塊需要哪些既有資料,又會產生哪些資料給後面使用?


糾纏式

第二種叫 Snarled Method,它的主要問題不是步驟排得太長,而是 ifforeachswitch 等控制流程彼此巢狀,一個判斷包住下一個判斷。

控制流程 是程式決定「下一步要執行哪一段」的路線,每一個 ifcase 都會把路線分成不同 branch(分支),當分支裡還有分支,就會形成巢狀結構,讀者必須同時記住外面每一層條件,才知道目前這幾行在什麼情況下會執行。

今日範例 中一段停車場月租車位申請流程 ParkingMonthlyService

public MonthlyPassResult ApplyMonthlyPass(MonthlyPassRequest request)
{
    var result = new MonthlyPassResult();
    if (request.StartDate >= DateTime.Today)
    {
        var slotsA = _slotRepo.GetAvailable(Zone.A, request.StartDate);
        if (slotsA.Count == 0)
        {
            // A 區額滿就走升等路線:掃 B 區看有沒有剩餘車位
            foreach (var slot in _slotRepo.GetAvailable(Zone.B, request.StartDate))
            {
                if (slot.AllowsUpgrade)
                {
                    switch (request.ApplicantType)
                    {
                        case ApplicantType.Corporate:
                            var quota = _quotaRepo.GetCorporateRemaining(request.CompanyId);
                            if (quota > 0)
                            {
                                if (IsPeakSeason(request.StartDate))
                                {
                                    // 旺季升等 B 區先收一個月訂金,管委會規定
                                    result.RequireDeposit = true;
                                    result.DepositAmount = slot.MonthlyRate;
                                }
                                result.SlotId = slot.Id;
                                result.Status = MonthlyPassStatus.Held;
                            }
                            else
                            {
                                result.Status = MonthlyPassStatus.WaitList;
                            }
                            break;
                        case ApplicantType.Residential:
                            // ...住戶升等條件另有四十行,巢得跟上面一樣深
                            break;
                    }
                }
                if (result.Status == MonthlyPassStatus.Held) break;
            }
        }
        else
        {
            // ...A 區還有位置的正常路線,又是一層一層的優先序判斷
        }
    }
    return result;
}

要讀到旺季訂金的部分,就得依序經過申請人送出申請開始,之後先查 A 區有沒有空位,A 區滿了就轉到 B 區走升等路線,升等路線又依申請人類型分成法人戶與住戶,法人戶還要查公司剩餘配額,旺季則要先收一個月訂金:

日期有效
└── A 區額滿
    └── B 區車位允許升等
        └── 申請人是法人戶
            └── 法人配額有剩
                └── 旺季才收訂金

只要其中一層不成立,程式就不會走到訂金規則,而每一層旁邊可能還有自己的 elsebreak 或另一套後續處理,所以糾纏式的閱讀重點通常會是:

我想處理的那一行,需要先通過哪些 branch 才到得了?

也就是閱讀方向可能得要由內向外閱讀。


讓 SonarQube 幫我們找怪獸

Legacy Code 這麼複雜,有沒有工具可以先幫我們把可能的問題先大概抓出來?

這時候就可以請 SonarQube 這個工具來幫忙,它會對程式碼進行靜態分析,在不執行程式的情況下,依照規則找出可靠性、安全性與可維護性等問題,我平常也會很常使用到它,好東西就要跟好朋友分享,至於要怎麼使用它呢,我們繼續看下去。

準備 Docker Desktop

首先我們需要透過 Docker 在本機啟動 SonarQube,如果還不熟悉容器技術,或電腦尚未安裝 Docker Desktop 的讀者嗎,可以先閱讀我另外整理的 Docker 基本與安裝 文章喔 > <

補充教材整理了 Container 的用途、Windows 與 macOS 安裝流程,以及安裝後的基本驗證,這裡就不贅述了,準備好 Docker 後我們就可以繼續往下了

在本機啟動 SonarQube

確認 Docker Engine 已經啟動後,在 Docker Desktop 中搜尋 sonarqube

https://ithelp.ithome.com.tw/upload/images/20260821/20182564RXKEGZwApd.png

按下 Pull 將官方鏡像抓取下來,Host port 設定 9000 就好了,接著按下 Run

https://ithelp.ithome.com.tw/upload/images/20260821/20182564Y12oM3gX9f.png

等到容器啟動後,就可以用瀏覽器打開 http://localhost:9000,預設帳號與密碼都會是 admin

https://ithelp.ithome.com.tw/upload/images/20260821/20182564gxFc3k6H0f.png

登入後系統會要求更換密碼。

建立專案

進入 SonarQube 主頁後按下 Create a local project

https://ithelp.ithome.com.tw/upload/images/20260821/20182564dro8oaUfP5.png

這一步是在 SonarQube 裡先建立一個接收分析結果的專案,還沒有開始掃描。

Project display name 是畫面上顯示的名稱,Project key 則是 SonarQube 辨識專案的唯一代號,等等執行 Scanner 時必須和這裡完全一致,Main branch name 填專案實際使用的主要分支,範例是 master,如果我們的 Repository 用的是 main,這裡就改成 main

https://ithelp.ithome.com.tw/upload/images/20260821/20182564T1r6MeNhdT.png

確認三個欄位後按下 Next,SonarQube 就知道稍後收到的分析資料應該放到哪個專案底下。

取得 Token

Scanner 最後要把分析結果送回 SonarQube,因此需要一個 Token 證明自己有權限執行。

右上角點選頭像到 My Account 頁面

https://ithelp.ithome.com.tw/upload/images/20260821/20182564pJNBqo7Tcs.png

進入 Security 分頁

https://ithelp.ithome.com.tw/upload/images/20260821/2018256455SAwdx6Ck.png

在 Generate Tokens 區域,輸入資訊後,點 Generate

https://ithelp.ithome.com.tw/upload/images/20260821/20182564F5JbkkwC3u.png

這裡選擇 Project Analysis Token,讓權限只落在 Day21-22,產製之後,Token 只會在產生後顯示一次,複製起來保管好,我們等等就會用到。

安裝 .NET Scanner

SonarQube 服務負責接收分析結果與顯示報告,真正跟著收集程式碼分析資料的則是 SonarScanner for .NET,先透過以下指令安裝它:

dotnet tool install --global dotnet-sonarscanner

開始分析

我們首先要先透過 begin 指令先連上 SonarQube,取得專案目前套用的 Quality Profile 與分析設定,再把 Scanner 接進接下來的 .NET build。

dotnet sonarscanner begin \
  /k:"Day21-22" \ 
  /d:sonar.token="請換成自己的Token" \
  /d:sonar.host.url="http://localhost:9000"

三個參數分別表示

分析結果要送到哪個 Project Key
用什麼憑證驗證
SonarQube 服務位址在哪裡

執行後如果看到 Pre-processing succeeded. 代表前置設定已經完成,Scanner 正在等接下來的編譯資料,還不是整次分析結束喔。

dotnet build --no-incremental

接著執行 build,讓 Scanner 在實際編譯過程中收集專案結構與 C# 分析結果。

dotnet sonarscanner end /d:sonar.token="請換成自己的Token"

最後的 end 會結束這次分析、清除 build 階段掛上的分析設定,收集剛才產生的結果並上傳到 SonarQube。

完成分析後,我們前往 SonarQube 的 Day21-22 中,左側欄可以看到一個 Issue 頁面

https://ithelp.ithome.com.tw/upload/images/20260821/20182564b3OcHmM7s9.png

這次一共找到了三個 Issue,其中第一個是 High,另外兩個則是把 IsPeakSeason() 改成 static 的 Low,以及命名規則的 Info,這三條是目前 Quality Profile 啟用的規則所提出的檢查結果。

我們先點開第一個 High 來看看。

https://ithelp.ithome.com.tw/upload/images/20260821/20182564MKr7GjB45K.png

畫面會直接帶到 ParkingMonthlyService.cs 的程式碼,並標出這個 method 裡讓複雜度增加的 if、foreach 與 switch,點開 How can I fix it 頁面,還可以看到它給我們的建議,雖然就當作參考就是了 XD

https://ithelp.ithome.com.tw/upload/images/20260821/20182564Wp7p7Vyv6V.png

是不是超酷的啊!

不過實際上出現 Issue 的評斷,還會受到目前使用的 Quality Profile、啟用規則與門檻影響,所以會依照實務上團隊的開發標準來決定品質門檻。

透過工具可以幫助我們發現不對勁的地方,這真的超級方便,但真正判斷它該從何下手?該不該動手?業務的語言是什麼?這些仍然需要人去理解。

SonarQube 的功能當然也不如此,功能超級多的,實務上我們也很常把它放進產線中,讓每次部署都能再做程式碼品質的各方位掃描,各位有興趣的話可以慢慢逛喔 :)


總結

在 Legacy Code 中,遇到的 Monster Method 很少是純種,往往同時可以看到好幾種特徵。

野生的通常都是混種

常見情況是外層看起來像條列式,一段一段往下排,但某個 method 裡是糾纏式,也可能反過來,在糾纏式的某個深層 branch 裡,埋著一堆條列流程。

聽著感覺挺可怕的,不過沒關係,今天我們先了解到這些 method ,也順便知道了 SonarQube 這個好東西後,接下來我們將學習如何改善它們。

明天我們繼續看:如何一步一步解開條列與糾纏

Reference


上一篇
Day 20 -「阿爾戈號」識破 God Class 的真面目(下):搬走了,怎麼還在?
下一篇
Day 22 -「安泰俄斯」識破 Monster Method 的真面目(下):解決之道
系列文
諸神也搖頭的 Legacy Code: 30天 .NET 工程師生存之道22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言